﻿using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;

namespace Granite.Wcf.PocoService
{
	/// <summary>
	/// Used in conjunction with PocoServiceHost to expose an arbitrary object as a service
	/// </summary>
	class PocoServiceBehavior : IServiceBehavior
	{
		//TODO: Make this a property
		const string DefaultNamespace = "http://tempuri.org/";

		//TODO: Make this a property
		static readonly Func<Binding> createBinding = () => new BasicHttpBinding();

		/// <summary>
		/// Set to Null to use the default instance provider. 
		/// </summary>
		public IInstanceProvider InstanceProvider { get; set; }

		/// <summary>
		/// Provides the ability to pass custom data to binding elements to support the
		/// contract implementation.
		/// </summary>
		/// <param name="serviceDescription">The service description of the service.</param>
		/// <param name="serviceHostBase">The host of the service.</param>
		/// <param name="endpoints">The service endpoints.</param>
		/// <param name="bindingParameters">Custom objects to which binding elements have access.</param>
		public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
		{
		}

		/// <summary>
		/// Provides the ability to change run-time property values or insert custom extension objects such as error handlers, message or parameter interceptors, security extensions, and other custom extension objects.
		/// </summary>
		/// <param name="serviceDescription">
		/// The service description.
		/// </param>
		/// <param name="serviceHostBase">
		/// The host that is currently being built.
		/// </param>
		public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
		{
			var contractDescription = CreateContractDescription(serviceDescription.ServiceType);

			//TODO: Shouldn't this be done in a loop so get all of the addresses?
			//TODO: What if we have multiple bindings?

			var endpointAddress = serviceHostBase.BaseAddresses[0].AbsoluteUri;

			var endpoint = new ServiceEndpoint(contractDescription, createBinding(), new EndpointAddress(new Uri(endpointAddress)));
			serviceDescription.Endpoints.Add(endpoint);

			var dispatcher = CreateChannelDispatcher(endpoint, serviceDescription.ServiceType);
			serviceHostBase.ChannelDispatchers.Add(dispatcher);

			AssociateEndpointToDispatcher(endpoint, dispatcher);
		}

		/// <summary>
		/// Provides the ability to inspect the service host and the service description to confirm that the service can run successfully.
		/// </summary>
		/// <param name="serviceDescription">
		/// The service description.
		/// </param>
		/// <param name="serviceHostBase">
		/// The service host that is currently being constructed.
		/// </param>
		public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
		{
			var serviceType = serviceDescription.ServiceType;

			var ctor = serviceType.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, new Type[0], null);
			if (InstanceProvider == null && ctor == null)
				throw new InvalidOperationException("Service must have a parameterless, public constructor OR a custom instance provider.");

			var methods = serviceType.GetMethods(BindingFlags.Instance | BindingFlags.Public).Where(m => m.DeclaringType == serviceType).ToArray();
			if (methods.Length == 0)
				throw new InvalidOperationException("Service does not have any public methods.");

			foreach (var method in methods)
				foreach (var parameter in method.GetParameters())
					if (parameter.ParameterType.IsByRef)
						throw new InvalidOperationException("This behavior does not support public methods with out/ref parameters.");
		}

		void AssociateEndpointToDispatcher(ServiceEndpoint endpoint, ChannelDispatcher dispatcher)
		{
			// This is a workaround to the fact that the id of the dispatcher and endpoint must match
			// for the endpoint to be exposed in the service metadata. For simply using the service,
			// this step is not necessary.

			var instanceBindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
			var endpointIdProperty = typeof(ServiceEndpoint).GetProperty("Id", instanceBindingFlags);
			var endpointDispatcherIdProperty = typeof(EndpointDispatcher).GetProperty("Id", instanceBindingFlags);
			var endpointId = endpointIdProperty.GetValue(endpoint, null) as string;
			foreach (var ed in dispatcher.Endpoints)
			{
				endpointDispatcherIdProperty.SetValue(ed, endpointId, null);
			}
		}

		ChannelDispatcher CreateChannelDispatcher(ServiceEndpoint endpoint, Type serviceType)
		{
			var address = endpoint.Address;
			var bindingParameters = new BindingParameterCollection();
			var channelListener = endpoint.Binding.BuildChannelListener<IReplyChannel>(address.Uri, bindingParameters);
			var channelDispatcher = new ChannelDispatcher(channelListener, endpoint.Binding.Namespace + ":" + endpoint.Binding.Name, endpoint.Binding);
			channelDispatcher.MessageVersion = endpoint.Binding.MessageVersion;

			var endpointDispatcher = new EndpointDispatcher(address, endpoint.Contract.Name, endpoint.Contract.Namespace, false);
			foreach (var operation in endpoint.Contract.Operations)
			{
				var replyAction = operation.Messages.Count > 1 ? operation.Messages[1].Action : "";
				var operationDispatcher = new DispatchOperation(endpointDispatcher.DispatchRuntime, operation.Name, operation.Messages[0].Action, ((string)replyAction));
				foreach (var operationBehavior in operation.Behaviors)
					operationBehavior.ApplyDispatchBehavior(operation, ((DispatchOperation)operationDispatcher));

				endpointDispatcher.DispatchRuntime.Operations.Add(((DispatchOperation)operationDispatcher));
			}

			if (InstanceProvider != null)
				endpointDispatcher.DispatchRuntime.InstanceProvider = InstanceProvider;
			else
				endpointDispatcher.DispatchRuntime.InstanceProvider = new Granite.Wcf.PocoService.DefaultInstanceProvider(serviceType);

			endpointDispatcher.DispatchRuntime.InstanceContextProvider = new Granite.Wcf.PocoService.SimpleInstanceContextProvider();
			endpointDispatcher.AddressFilter = new EndpointAddressMessageFilter(endpoint.Address);
			endpointDispatcher.ContractFilter = new ActionMessageFilter(endpoint.Contract.Operations.Select(op => op.Messages[0].Action).ToArray());
			endpointDispatcher.FilterPriority = 1;

			channelDispatcher.Endpoints.Add(endpointDispatcher);
			return channelDispatcher;
		}

		static ContractDescription CreateContractDescription(Type serviceType)
		{
			var result = new ContractDescription(serviceType.Name, DefaultNamespace);
			result.ContractType = serviceType;
			foreach (var method in serviceType.GetMethods(BindingFlags.Instance | BindingFlags.Public))
			{
				if (method.DeclaringType == serviceType)
				{
					result.Operations.Add(CreateOperationDescription(result, serviceType, method));
				}
			}

			return result;
		}

		static OperationDescription CreateOperationDescription(ContractDescription contract, Type serviceType, MethodInfo method)
		{
			var result = new OperationDescription(method.Name, contract);
			result.SyncMethod = method;

			var inputMessage = new MessageDescription(DefaultNamespace + serviceType.Name + "/" + method.Name, MessageDirection.Input);
			inputMessage.Body.WrapperNamespace = DefaultNamespace;
			inputMessage.Body.WrapperName = method.Name;
			var parameters = method.GetParameters();
			for (int i = 0; i < parameters.Length; i++)
			{
				var parameter = parameters[i];
				var part = new MessagePartDescription(parameter.Name, DefaultNamespace);
				part.Type = parameter.ParameterType;
				part.Index = i;
				inputMessage.Body.Parts.Add(part);
			}

			result.Messages.Add(inputMessage);

			var outputMessage = new MessageDescription(DefaultNamespace + serviceType.Name + "/" + method.Name + "Response", MessageDirection.Output);
			outputMessage.Body.WrapperName = method.Name + "Response";
			outputMessage.Body.WrapperNamespace = DefaultNamespace;
			outputMessage.Body.ReturnValue = new MessagePartDescription(method.Name + "Result", DefaultNamespace);
			outputMessage.Body.ReturnValue.Type = method.ReturnType;
			result.Messages.Add(outputMessage);

			result.Behaviors.Add(new Granite.Wcf.PocoService.OperationInvoker(method));
			result.Behaviors.Add(new OperationBehaviorAttribute());
			result.Behaviors.Add(new DataContractSerializerOperationBehavior(result));

			return result;
		}

	}
}